﻿
Imports std
Imports storoomc
Imports xmlctrl
Imports usshare

Public MustInherit Class servelib
    Inherits controlProcessor

    Public Const maxId As Byte = 62
    Private Shared initializedRecordCount As Boolean = False
    Private randomkeylen As Int32
    Private minkeylen As Int32
    Private keyVisitCounterLink As String = Nothing
    Private keyVisitCounterApiLink As String = Nothing
    Public Const keyVisitCounterLinkKey As String = "{key}"

    Private Class storoomserver
        Public Shared host As String
        Public Shared port As Int32
        Public Shared retrytimes As UInt64
    End Class

    Private Function getstoroom(ByRef host As String, ByRef port As Int32, ByRef retrytimes As UInt64) As Boolean
        copy(host, storoomserver.host)
        copy(port, storoomserver.port)
        copy(retrytimes, storoomserver.retrytimes)

        Return True
    End Function

    Private Sub initialRecordCount()
        recordCount = 0
        While Not storoom.keycount(recordCount)
            sleep()
        End While
        'fake a data now, since the count of records is small
        recordCount <<= 1
    End Sub

    Public Overrides Sub initial(Optional ByVal config As configParser = Nothing)
        'ATTENTION, this initial function will be called by each servelib child class, should improve.
        MyBase.initial(config)
        assert(Not config Is Nothing, "urlshorten.serverlib should be initialized with a valid configParser.")
        storoomserver.host = config.parameter(configNaming.urlshorten.name, configNaming.urlshorten.storoomhost, _
                                              configNaming.defaultValue.urlshorten.storoomhost)
        storoomserver.port = config.parameter(configNaming.urlshorten.name, configNaming.urlshorten.storoomport, _
                                              configNaming.defaultValue.urlshorten.storoomport)
        storoomserver.retrytimes = config.parameter(configNaming.urlshorten.name, _
                                                    configNaming.urlshorten.storoomretrytimes, _
                                                    configNaming.defaultValue.urlshorten.storoomretrytimes)
        randomkeylen = config.parameter(configNaming.urlshorten.name, _
                                        configNaming.urlshorten.randomKeyLength, _
                                        configNaming.defaultValue.urlshorten.randomKeyLength)
        minkeylen = config.parameter(configNaming.urlshorten.name, _
                                     configNaming.urlshorten.minKeyLength, _
                                     configNaming.defaultValue.urlshorten.minKeyLength)
        keyVisitCounterLink = config.parameter(configNaming.urlshorten.name, _
                                               configNaming.urlshorten.keyVisitCounterLink, _
                                               configNaming.defaultValue.urlshorten.keyVisitCounterLink)
        keyVisitCounterApiLink = config.parameter(configNaming.urlshorten.name, _
                                                  configNaming.urlshorten.keyVisitCounterApiLink, _
                                                  configNaming.defaultValue.urlshorten.keyVisitCounterApiLink)

        storoom.initial(AddressOf getstoroom)

        If Not initializedRecordCount Then
            initialRecordCount()
            initializedRecordCount = True
        End If
    End Sub

    Private Function isInvalidKey(ByVal key As String)
        Return strlen(key) < minkeylen
    End Function

    Private Function id2char(ByVal id As Byte) As Char
        assert(id >= 0 AndAlso id < maxId, "should not have an id which is not in range [0,62).")

        If id < 10 Then
            Return Convert.ToChar(id + Convert.ToInt32(character._0))
        ElseIf id < 36 Then
            Return Convert.ToChar(id + Convert.ToInt32(character.a) - 10)
        ElseIf id < 62 Then
            Return Convert.ToChar(id + Convert.ToInt32(Char.ToUpper(character.a)) - 36)
        Else
            assert(False, "the maxId has not been included in id2char.")
        End If
    End Function

    Public Function generateRandomKey() As String
        Dim i As Int32
        Dim rtn As String = Nothing
        For i = 0 To randomkeylen - 1
            rtn += id2char(rnd(0, maxId, True))
        Next

        Return rtn
    End Function

    Private Function generateVisitCountLink(ByVal baselink As String, ByVal key As String) As String
        Dim rtn As String = Nothing
        copy(rtn, baselink)
        strrplc(rtn, keyVisitCounterLinkKey, key)

        Return rtn
    End Function

    Protected Function generateVisitCountLink(ByVal key As String) As String
        Return generateVisitCountLink(keyVisitCounterLink, key)
    End Function

    Protected Function generateVisitCountApiLink(ByVal key As String) As String
        Return generateVisitCountLink(keyVisitCounterApiLink, key)
    End Function

    Protected Sub pingVisitCount(ByVal key As String)
        'avoid to holding thread, so set timeout to 1s
        If Not pingHttpWeb(generateVisitCountApiLink(key), Nothing, timeoutMS:=1000) Then
            raiseError("cannot request visitCounter for key " + key + _
                       ", full address " + generateVisitCountApiLink(key), _
                       errorHandle.errorType.warning)
        End If
    End Sub

    Private Function key2url(ByVal key As String, ByRef url As String) As Boolean
        Dim bt As bytes = Nothing
        If storoom.read(key, bt) Then
            url = encoding.utf8.GetString(+bt)
            urlUtils.urlnormalize(url)
            Return True
        Else
            Return False
        End If
    End Function

    Private Function url2keys(ByVal url As String, ByRef keys As [set](Of String)) As Boolean
        If keys Is Nothing Then
            keys = New [set](Of String)()
        Else
            keys.clear()
        End If
        Dim buff As bytes = Nothing
        If Not storoom.read(url, buff) Then
            Return False
        End If

        Dim keyall As String = Nothing
        keyall = encoding.utf8.GetString(+buff)
        Dim keyvec As vector(Of String) = Nothing
        strsepar(keyall, character.newline, keyvec, True)

        Dim i As Int64
        For i = 0 To keyvec.size() - 1
            kickLeftRightBlank(keyvec(i))
            keys.insert(keyvec(i))
        Next

        Return keys.size() > 0
    End Function

    Private Function have(ByVal url As String) As Boolean
        Return storoom.seek(url)
    End Function

    Private Function submit(ByRef key As String, ByVal url As String) As submitResult
        If isemptystring(key) Then
            Do
                key = generateRandomKey()
            Loop Until Not storoom.seek(key)
        Else
            If isInvalidKey(key) Then
                Return submitResult.invalidKey
            End If

            If storoom.seek(key) Then
                Return submitResult.existingKey
            End If
        End If
        If storoom.modify(key, New bytes(encoding.utf8.GetBytes(url))) _
           AndAlso storoom.write(url, New bytes(encoding.utf8.GetBytes(key + character.newline))) Then
            Return submitResult.success
        Else
            Return submitResult.internalError
        End If
    End Function

    'this generator always use urlshorten html service as referer
    Protected Class urlgenerator
        Public Shared Function preview(ByVal key As String) As String
            Return urlshorten._pathidentity + urlUtils.combineQuery(query.key, key _
                                                                  , query.action, actionVerbs(action.preview))
        End Function

        Public Shared Function jump(ByVal key As String) As String
            Return urlshorten._pathidentity + urlUtils.combineQuery(query.key, key _
                                                                  , query.action, actionVerbs(action.jump))
        End Function

        Public Shared Function jumpfull(ByVal key As String) As String
            Dim context As httpContext = Nothing
            context = httpServer2.currentContext()
            assert(Not context Is Nothing, "cannot get current http context.")
            'use pathidentity in urlshorten <html service>, so it can jump
            'need to handle key urlencoding by this function itself
            Return strleft(context.url().full(), strlen(context.url().full()) - strlen(context.url().pathquery())) _
                   + urlshorten._pathidentity + urlUtils.urlencode(key)
        End Function

        Public Shared Function delayjump(ByVal key As String) As String
            Return urlshorten._pathidentity + urlUtils.combineQuery(query.key, key _
                                                                  , query.action, actionVerbs(action.delayJump))
        End Function

        Public Shared Function submit(ByVal url As String, ByVal key As String) As String
            Return urlshorten._pathidentity + urlUtils.combineQuery(query.key, key _
                                                                  , query.url, url _
                                                                  , query.action, actionVerbs(action.submit))
        End Function

        Public Shared Function urlkeys(ByVal url As String) As String
            Return urlshorten._pathidentity + urlUtils.combineQuery(query.url, url _
                                                                  , query.action, actionVerbs(action.urlkeys))
        End Function

        Public Shared Function have(ByVal url As String) As String
            Return urlshorten._pathidentity + urlUtils.combineQuery(query.url, url _
                                                                  , query.action, actionVerbs(action.have))
        End Function
    End Class

    Protected MustOverride Function jump(ByVal context As httpContext, ByRef control As xmlcontrol _
                                         , ByVal key2urlResult As Boolean _
                                         , ByVal key As String, ByVal url As String, ByVal delay As Int64) As Boolean

    Protected MustOverride Function preview(ByVal context As httpContext, ByRef control As xmlcontrol _
                                            , ByVal key2urlResult As Boolean _
                                            , ByVal key As String, ByVal url As String) As Boolean

    Protected MustOverride Function submit(ByVal context As httpContext, ByRef control As xmlcontrol _
                                           , ByVal submitResult As submitResult _
                                           , ByVal key As String, ByVal url As String) As Boolean

    Protected MustOverride Function urlkeys(ByVal context As httpContext, ByRef control As xmlcontrol _
                                            , ByVal url2keysResult As Boolean _
                                            , ByVal url As String, ByVal keys As [set](Of String)) As Boolean

    Protected MustOverride Function homepage(ByVal context As httpContext, ByRef control As xmlcontrol) As Boolean

    Protected MustOverride Function have(ByVal context As httpContext, ByRef control As xmlcontrol _
                                         , ByVal haveResult As Boolean, ByVal url As String) As Boolean

    Protected NotOverridable Overrides Function processImpl(ByVal context As httpContext _
                                                            , ByRef control As xmlcontrol) As Boolean
        assert(Not context Is Nothing, "context is a nothing httpContext instance.")
        context.addResponseHeader(Net.HttpResponseHeader.Server, applicationDescription())

        Dim key As String = Nothing
        Dim url As String = Nothing
        Dim action As action = action.unknown

        If strlen(context.url.path) > strlen(pathidentity()) Then
            key = urlUtils.urldecode(strmid(context.url.path, strlen(pathidentity())), encoding.utf8)
            action = action.jump
        Else
            Dim actionVerb As String = Nothing
            actionVerb = context.url.query(query.action)
            Dim i As Int64
            For i = 0 To actionVerbs.Length() - 1
                If strsame(actionVerb, actionVerbs(i), False) Then
                    action = i
                    Exit For
                End If
            Next

            url = context.url.query(query.url)
            key = context.url.query(query.key)

            If action = action.unknown Then
                If Not isemptystring(key) Then
                    action = action.preview
                ElseIf Not isemptystring(url) Then
                    action = action.submit
                End If
            End If
        End If
        If Not isemptystring(url) Then
            urlUtils.urlnormalize(url)
        End If

        Select Case action
            Case action.jump, action.delayJump
                Return jump(context, control, key2url(key, url), key, url _
                            , ifoper(action = action.jump, 0, 10))
            Case action.preview
                Return preview(context, control, key2url(key, url), key, url)
            Case action.submit
                recordCount += 1
                Return submit(context, control, submit(key, url), key, url)
            Case action.urlkeys
                Dim keys As [set](Of String) = Nothing
                Return urlkeys(context, control, url2keys(url, keys), url, keys)
            Case action.have
                Return have(context, control, have(url), url)
            Case action.unknown
                Return homepage(context, control)
            Case Else
                assert(False, "all actions should be covered in select case.")
        End Select
    End Function
End Class
